Skip to content

S08-01 Node-基础

[TOC]

概述

Atwood 定律

Atwood 定律:任何可以使用 JavaScript 来实现的应用都最终都会使用 JavaScript 实现。

Stack Overflow 的创立者之一的 Jeff Atwood 在 2007 年提出了著名的 Atwood 定律:

  • Any application that can be written in JavaScript, will eventually be written in JavaScript.
  • 任何可以使用 JavaScript 来实现的应用都最终都会使用 JavaScript 实现。

但是在发明之初,JavaScript 的目的是应用于在浏览器执行简单的脚本任务,对浏览器以及其中的 DOM 进行各种操作,所以 JavaScript 的应用场景非常受限。

  • Atwood 定律更像是一种美好的远景,在当时看来还没有实现的可能性。
  • 但是随着 Node 的出现,Atwood 定律已经越来越多的被证实是正确的。

但是为了可以理解 Node.js 到底是如何帮助我们做到这一点的,我们必须了解 JavaScript 是如何被运行的。

浏览器内核

浏览器内核:又名排版引擎(layout engine),也称为浏览器引擎(browser engine)、页面渲染引擎(rendering engine)或样版引擎

浏览器内核-分类:

  • Gecko:早期被 Netscape 和 Mozilla Firefox 浏览器使用;
  • Trident:微软开发,被 IE4~IE11 浏览器使用,但是 Edge 浏览器已经转向 Blink;
  • Webkit:苹果基于 KHTML 开发、开源的,用于 Safari,Google Chrome 之前也在使用;
  • Blink:是 Webkit 的一个分支,Google 开发,目前应用于 Google Chrome、Edge、Opera 等;

浏览器内核-组成:

  • WebCore:负责 HTML 解析、布局、渲染等等相关的工作;
  • JavaScriptCore:解析、执行 JavaScript 代码;

小程序中编写的 JavaScript 代码就是被 JSCore 执行的

image-20240117133227786

原理图:

image-20240719154519878

但是在这个执行过程中,HTML 解析的时候遇到了 JavaScript 标签,应该怎么办呢?

  • 会停止解析 HTML,而去加载和执行 JavaScript 代码;

当然,为什么不直接异步去加载执行 JavaScript 代码,而要在这里停止掉呢?

  • 这是因为 JavaScript 代码可以操作我们的 DOM;
  • 所以浏览器希望将 HTML 解析的 DOM 和 JavaScript 操作之后的 DOM 放到一起来生成最终的 DOM 树,而不是频繁的去生成新的 DOM 树;

那么,JavaScript 代码由谁来执行呢?

  • JavaScript 引擎

JS 引擎

JS 引擎作用:JS 引擎帮助我们将 JS 代码翻译成 CPU 指令来执行

事实上我们编写的 JavaScript 无论你交给浏览器或者 Node 执行,最后都是需要被 CPU 执行的。但是 CPU 只认识自己的指令集,实际上是机器语言。所以我们需要 JavaScript 引擎帮助我们将 JavaScript 代码翻译成 CPU 指令来执行。

常见 JS 引擎:

  • SpiderMonkey:第一款 JavaScript 引擎,由 Brendan Eich 开发(也就是 JavaScript 作者);
  • Chakra:微软开发,用于 IE 浏览器;
  • JavaScriptCore:WebKit 中的 JavaScript 引擎,Apple 公司开发;
  • V8:Google 开发的强大 JavaScript 引擎,也帮助 Chrome 从众多浏览器中脱颖而出;

下面来详细介绍一下 V8 引擎。

V8

V8:是用 C ++编写的 Google 开源高性能 JavaScript 和 WebAssembly 引擎,它用于ChromeNode.js等。它实现 ECMAScript 和 WebAssembly,并在 Windows 7 或更高版本,macOS 10.12+和使用 x64,IA-32,ARM 或 MIPS 处理器的 Linux 系统上运行。V8 可以独立运行,也可以嵌入到任何 C ++应用程序中


V8 原理图

image-20240719154534525

V8 引擎本身的源码非常复杂,大概有超过 100w 行 C++代码,但是我们可以简单了解一下它执行 JavaScript 代码的原理:

  • Parse模块会将 JS 代码转换成 AST(抽象语法树),这是因为解释器并不直接认识 JavaScript 代码;

  • Ignition是一个解释器,会将 AST 转换成 ByteCode(字节码),同时会收集 TurboFan 优化所需要的信息(比如函数参数的类型信息,有了类型才能进行真实的运算)

  • TurboFan是一个编译器,可以将字节码编译为 CPU 可以直接执行的机器码

    • 注意 :如果一个函数被多次调用,那么就会被标记为热点函数,那么就会经过 TurboFan 转换成优化的机器码,提高代码的执行性能;

    • 但是,机器码实际上也会被还原为 ByteCode,这是因为如果后续执行函数的过程中,类型发生了变化(比如 sum 函数原来执行的是 number 类型,后来执行变成了 string 类型),之前优化的机器码并不能正确的处理运算,就会逆向的转换成字节码;

    • TurboFan 的 V8 官方文档:https://v8.dev/blog/turbofan-jit

  • Orinoco 事实上 V8 的内存回收也是其强大的另外一个原因,这里暂时先不展开讨论:

Node 基础

概述

Node.js:是一个基于 V8 JavaScript 引擎的 JavaScript 运行时环境


浏览器、Node 组成

  • 浏览器组成:

    • V8 引擎:运行 JS

    • DOM:解析渲染 HTML、CSS

    • BOM:浏览器对象模型(浏览器 API)

  • Node 组成:

    • V8 引擎

    • 文件系统读/写

    • 网络 IO

    • 加密

    • 压缩解压文件

image-20240719154547817


Node 架构图

image-20240719154558375

  • 我们编写的 JavaScript 代码会经过 V8 引擎,再通过 Node.js 的 Bindings,将任务放到 Libuv 的事件循环中;
  • libuv(Unicorn Velociraptor—独角伶盗龙)是使用 C 语言编写的库;
  • libuv 提供了事件循环文件系统读写网络 IO线程池等等内容;
  • 具体内部代码的执行流程,我会在后续专门讲解事件和异步 IO 的原理中详细讲解;

Node 应用场景

  • 包管理工具
    • npm
    • yarn
    • pnpm
  • 脚手架
    • webpack
    • vite
    • gulp
  • 服务器
    • web 服务器
    • 代理服务器
    • 中间件
  • SSR
  • 脚本工具
  • 工程自动化
  • Electron
    • vscode

依赖安装

依赖包: Node.js

版本:

  • LTS:稳定版
  • Current:最新版

安装: 和在 windows 上安装其他软件一样

常用命令:

  • node --version:查看当前的版本
  • node <path/to/index.js>运行 index.js 文件

基本使用

Node 的基本使用

  1. 编写 JS 代码

    abc.js 中编写 JS 代码

    js
    console.log('a')
    console.log('b')
    console.log('c')
  2. 终端运行

    通过 node <path> 命令执行 JS 文件

    image-20251013113535618

版本管理工具

n

介绍: Linux 环境的 Node 版本管理工具

安装: npm i n -g

常用命令:

  • n --version,查看安装的版本
  • n lts,安装最新的 LTS 版本
  • n latest,安装最新的版本
  • n,查看所有的版本

权限获取

  • sodu:Linux 环境中可以通过添加 sudo 前缀,获取执行命令时所需要的权限

nvm

介绍:Linux 环境的 Node 版本管理工具

nvm-window

介绍:Windows 环境的 Node 版本管理工具

常用命令:

查看 node

  • nvm list,查看所有安装的版本
  • nvm list installed,显示所有已安装的版本
  • nvm list available,显示所有可以下载的版本

安装 node

  • nvm install 16.9.0,安装 16.9.0 版本
  • nvm install 16,安装 16 大版本的最新小版本
  • nvm install lts,安装最新的 LTS 版本
  • nvm install latest,安装最新的版本(不是current

卸载 node

  • nvm uninstall <版本号>,卸载指定版本的 node

使用指定版本的 node

  • nvm use <版本号>,使用指定版本的 node

查看 nvm 版本

  • nvm version,查看 nvm 版本
  • nvm --version,查看 nvm 版本

终端

常用终端

  • CMD
  • PowerShell
  • Git Bash

VSCode 设置默认终端

  1. 打开 VSCode 终端,按如下顺序点击:

    image-20251013121046247

  2. 选择默认的终端:

    image-20251013120841153

Node 的输入、输出

输入

1、在终端中运行以下命令:node <index.js> num1=100 num2=200

2、在 js 中通过process.argv获取终端中输入的参数num1num2

image-20240117140148632


输出

  • console.log('hello'),打印消息
  • console.clear(),清空控制台
  • console.trace(),打印执行调用栈

REPL

REPL(Read-Eval-Print Loop,“读取-求值-输出”循环):是一个简单的、交互式的编程环境

常用命令:

  • 开启 REPL
    • node + 回车
  • 退出 REPL
    • Ctrl + C(按2次)
    • .exit
  • 清空控制台
    • CMD:cls
    • Git Bash:Ctrl + L
    • Linux:clear

示例:在 REPL 中的基本操作

image-20251013122837386

全局对象

环境变量

process.env

process.env:当前进程的环境变量。

process.argv

process.argv:一个包含命令行参数的数组。当在命令行中执行 Node.js 脚本时,可以使用该数组来访问传递给脚本的参数。

参数: 接受如下命令行中传递的参数:node <index.js> arg1=xxx arg2=xxx

返回值: 一个包含命令行参数的数组:

  • 第一个元素:Node.js 的可执行文件路径
  • 第二个元素:被执行的 JavaScript 文件的路径
  • 后续元素:命令行传递的参数

示例:process.argv

image-20240117141038270

image-20240117141003628

类似 window

window

window:浏览器环境下的全局对象,Node 环境下没有

注意: 通过 var 定义的变量,会被放入到 window 对象上

global

global:Node 环境下的全局对象,浏览器环境下没有

注意: 通过 var 定义的变量,并不会被放入到 global 对象上


示例:global 对象

image-20240117142713765

image-20251013143646661

globalThis

globalThisES2020,是一个跨平台的解决方案。浏览器和 Node 环境下分别指向全局对象 window 和 global

注意:

  • globalThis === global,Node 环境中二者等价
  • globalThis === window,浏览器环境中二者等价

模块

注意: 以下实际上是模块中的变量

require()

require()(id),用于加载和引用其他 JS 文件或模块

  • idstring,要加载的模块名称或路径。

  • 返回:

  • moduleany,已加载模块的对象。

示例:基本使用

js
const path = require('path')
exports

说明: 包含模块导出内容的空对象。通过向 exports 对象添加属性或方法,可以将它们导出给其他模块使用。

语法:

js
exports.xxx = value
exports.xxx = function() { ... }
exports.xxx = () => { ... }
module

说明: 表示当前模块的对象。每个 JS 文件都是一个独立的模块,可以通过 module 对象来访问和控制模块的行为。

语法:

js
module.exports = {
  xxx: value
}

属性:

  • module.exports:导出模块内容给其他模块使用。
  • module.id:表示当前模块的标识符,通常是文件的绝对路径。
  • module.filename:表示当前模块的文件名,通常是文件的绝对路径。
  • module.loaded:一个布尔值,表示当前模块是否已经加载完成。
  • module.parent:表示当前模块的父级模块。
  • module.children:表示当前模块依赖的子模块列表。

方法:

  • module.require(id):类似于全局的 require() 函数,用于加载和返回指定模块。
__dirname

说明: 当前文件所在目录(绝对路径)

语法:

js
// /home/user/projects/myapp/app.js
console.log(__dirname) // /home/user/projects/myapp
__filename

说明: 当前文件所在目录+文件名称(绝对路径)

语法:

js
// /home/user/projects/myapp/app.js
console.log(__filename) // /home/user/projects/myapp/app.js

URL

URLSearchParams

说明: 处理 URL 查询字符串的接口

语法:

js
const params = new URLSearchParams(init?)

参数:

  • init?string|object|URLSearchParams,被解析的目标。
    • string:它会被解析为查询参数,并用于初始化 URLSearchParams 对象。
    • object :它会被解析为键值对,并用于初始化 URLSearchParams 对象。
    • URLSearchParams:它会被复制到新的 URLSearchParams 对象。

返回值:

  • paramsURLSearchParams,可以使用 URLSearchParams 的方法来操作查询参数。

实例方法:

  • params.append(name, value):向查询参数中添加一个新的键值对。
  • params.delete(name):从查询参数中删除指定名称的键值对。
  • params.get(name)获取查询参数中指定名称的第一个值
  • params.getAll(name)获取查询参数中指定名称的所有值的数组。
  • params.has(name)检查查询参数中是否存在指定名称的键值对。
  • params.set(name, value):将查询参数中指定名称的键值对设置为新的值
  • params.sort():按照名称对查询参数进行排序
  • params.toString():返回表示查询参数的字符串

示例:解析查询字符串

js
var baseUrl = 'http://example.com/search?query=tom&age=33'

const url = new URL(baseUrl)
// 获取查询字符串
const queryString = url.search // ?query=tom&age=33
// 方式一:获取 URLSearchParams 对象
const query = url.searchParams // URLSearchParams { 'query' => 'tom', 'age' => '33' }
// 方式二:获取 URLSearchParams 对象
const params = new URLSearchParams(query) // URLSearchParams { 'query' => 'tom', 'age' => '33' }
// 转化URLSearchParams为对象格式
console.log(Object.fromEntries(params)) // { query: 'tom', age: '33' }

image-20250930153631712

定时器

  • window.setTimeout()(callback,delay?,...args?),用于在指定延迟时间后执行函数或代码的全局方法。
  • window.setInterval()(callback,delay,...args?),用于周期性无限循环调用函数或代码直到被显式取消的全局方法。
  • setImmediate()(callback,...args?)Node,用于安排回调函数在当前事件循环轮次的 "检查" 阶段立即执行的方法。
  • window.clearTimeout()(timeoutID),用于取消由 setTimeout() 创建的定时器的全局方法。可以阻止尚未执行的定时器回调函数的运行。
  • window.clearInterval()(intervalID),用于终止由 setInterval() 创建的周期性定时器的全局方法。
  • clearImmediate()(immediateID)Node,用于取消setImmediate() 创建的即将执行的回调函数的方法。

事件循环

定时器
queueMicrotask()

queueMicrotask()(callback)Node浏览器,用于将回调函数加入到微任务队列的全局函数。微任务会在当前 JS 执行栈清空后、事件循环继续之前执行。

  • callback()=>void,要在微任务队列中执行的回调函数。该函数不接受任何参数

示例

  1. 基本用法

    js
    queueMicrotask(() => {
      console.log('微任务执行');
    });
  2. 使用闭包传递数据

    js
    let data = '重要数据';
    queueMicrotask(() => {
      console.log(`处理数据: ${data}`);
    });
  3. 使用函数声明

    js
    function microtaskHandler() {
      console.log('这是一个微任务');
    }
    queueMicrotask(microtaskHandler);
  4. 在严格模式下的 this 行为

    js
    'use strict';
    queueMicrotask(function() {
      console.log(this); // 输出: undefined
    });

核心特性

  1. 微任务执行时机

    微任务在 JS 事件循环中的位置如下:同步代码执行nextTick队列微任务队列事件循环下一阶段

    js
    console.log('脚本开始');
    
    setTimeout(() => {
      console.log('setTimeout - 宏任务');
    }, 0);
    
    queueMicrotask(() => {
      console.log('queueMicrotask - 微任务');
    });
    
    process.nextTick(() => {
      console.log('process.nextTick - 最高优先级');
    });
    
    Promise.resolve().then(() => {
      console.log('Promise.then - 微任务');
    });
    
    console.log('脚本结束');
    
    // 输出顺序:
    //   脚本开始
    //   脚本结束
    //   process.nextTick - 最高优先级
    //   queueMicrotask - 微任务
    //   Promise.then - 微任务
    //   setTimeout - 宏任务
  2. 对比 process.nextTick()

    • process.nextTick(): Node.js 特有,优先级最高。
    • queueMicrotask(): 浏览器和 Node.js 通用,优先级次之。
  3. 对比 Promise.then()

    queueMicrotask() 可以看作是 Promise.resolve().then()语法糖,但更加直观和高效。

    js
    // 以下两种方式是等价的:
    queueMicrotask(() => {
      console.log('使用 queueMicrotask');
    });
    
    Promise.resolve().then(() => {
      console.log('使用 Promise.resolve().then()');
    });

注意事项

  1. 无法取消

    queueMicrotask() 没有返回标识符,因此无法取消已经排队的微任务,一旦调用,微任务必定会在当前执行栈结束后执行。

process.nextTick()

process.nextTick()(callback, ...args?)Node,用于将回调函数延迟到当前宏任务的末尾、微任务继续之前执行的函数。

  • callbackFunction,当前执行栈结束后要执行的函数。

  • ...args?any,调用回调时要传递的可选参数。

示例

  1. 基本用法

    js
    // 1. 不带参数
    process.nextTick(() => {
      console.log('在下一个Tick执行');
    });
    js
    // 2. 带一个参数
    process.nextTick((name) => {
      console.log(`Hello, ${name}!`);
    }, 'Alice');
    js
    // 3. 带多个参数
    process.nextTick((a, b, c) => {
      console.log(`计算结果: ${a + b * c}`);
    }, 2, 3, 4);
    js
    // 4. 使用箭头函数和普通函数
    process.nextTick(function() {
      console.log('这是一个普通函数');
    });

核心特性

  1. 执行时机

    1. process.nextTick() 不属于事件循环的任何阶段(定时器、I/O、检查等),它有一个独立的 "nextTick 队列"

    2. 在事件循环的每个阶段之间,Node.js 会检查 nextTick 队列,并执行其中的所有回调:

      当前JS代码执行nextTick队列微任务队列事件循环下一阶段

    js
    console.log('开始');
    
    setTimeout(() => {
      console.log('setTimeout');
    }, 0);
    
    setImmediate(() => {
      console.log('setImmediate');
    });
    
    process.nextTick(() => {
      console.log('nextTick');
    });
    
    console.log('结束');
    
    // 输出顺序:
    //   开始
    //   结束
    //   nextTick
    //   setTimeout 或 setImmediate(顺序不确定)
    //   setImmediate 或 setTimeout(顺序不确定)
  2. 对比 setImmediate()

    process.nextTick() 的优先级 高于 setImmediate()

    • process.nextTick()
      • 不属于事件循环的任何阶段,它有一个独立的 "nextTick 队列"
      • 会在事件循环当前阶段结束后、进入下一个阶段之前立即执行。
    • setImmediate()
      • 会在 事件循环的 "检查" 阶段 执行。
    js
    setImmediate(() => {
      console.log('setImmediate');
    });
    
    process.nextTick(() => {
      console.log('nextTick');
    });
    
    console.log('当前执行栈');
    // 输出顺序(确定性的):
    // 当前执行栈
    // nextTick
    // setImmediate
  3. 对比 Promise.then()

    执行优先级: process.nextTick() > Promise.then()

    js
    console.log('开始');
    
    process.nextTick(() => {
      console.log('nextTick');
    });
    
    Promise.resolve().then(() => {
      console.log('Promise');
    });
    
    console.log('结束');
    
    // 输出顺序:
    //   开始
    //   结束
    //   nextTick
    //   Promise

注意事项

  1. 无法取消

    process.nextTick() 没有返回标识符,因此无法取消已经安排的回调,一旦安排,回调必定会在当前执行栈结束后执行。